using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;

namespace Microsoft.Samples.BizTalk.Adapter.Tcp
{
    /// <summary>
    ///     This class provides static methods that support socket I/O.
    /// </summary>
    /// <author>
    ///     Paul Brown
    /// </author>
    public class SocketIO
    {
        #region Public Enumeration

        /// <summary>
        ///     This enumeration defines the states that the messaging handling can be in during the message framing logic.
        /// </summary>
        public enum FrameStatus
        {
            MessageStartNotFound = 1,
            MessageStartFoundCreateMessage,
            MessageStartFound,
            MessageEndFound
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        ///     This method receives data on the socket until a timeout occurs or the socket was closed.  If it was in the middle
        ///     of receiving a message, it sets a flag indicating that the last stream isn't complete and the adapter should use
        ///     this to suspend the last stream rather than submit it.  Any data between end and start frame delimiters is discarded
        ///     as it is considered that it is not part of the message.
        /// </summary>
        public static Stream[] ReceiveData(IAdapter adapter, Socket socket, byte[] frameStart, byte[] frameEnd, Encoding dataEncoding, int dataBufferSize, bool acceptMultipleMessages, out bool lastStreamNotComplete)
        {
            if (adapter.SocketTraceSwitch.TraceVerbose)
                TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketAboutToReceive, ConfigurationHelper.GetHexString(frameStart), ConfigurationHelper.GetHexString(frameEnd), dataEncoding, dataBufferSize, acceptMultipleMessages);

            // Set up array of streams
            List<Stream> streams = new List<Stream>();

            // Are we in the middle of receiving a message?
            FrameStatus currentStatus = FrameStatus.MessageStartNotFound;
            lastStreamNotComplete = false;

            // Buffer
            byte[] buffer = new byte[dataBufferSize];
            byte[] previousBuffer = new byte[dataBufferSize];

            // Set up variables
            int bytesRead = 0;
            int pos = 0;
            int framePos = 0;
            int backFillPos = 0;

            try
            {
                // Use a memory stream as the network stream is not seekable and resubmitting failed batches requires
                // a seekable stream.  Should keep a flat memory model so just going to go with the memory stream for now.
                MemoryStream stream = null;

                // Read from the socket
                bool messageFound = false;
                while ((bytesRead = socket.Receive(buffer)) > 0)
                {
                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveBytes, bytesRead);

                    // Reset variables
                    pos = 0;
                    bool processedAllBytes = false;

                    // Keep looping until all bytes have been processed
                    while (!processedAllBytes)
                    {
                        switch (currentStatus)
                        {
                            case FrameStatus.MessageStartNotFound:

                                if (adapter.SocketTraceSwitch.TraceVerbose)
                                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveStateStartNotFound);

                                // Search for frame start delimiter discarding data until we find it
                                while (pos < bytesRead)
                                {
                                    if (buffer[pos++] == frameStart[framePos])
                                    {
                                        // Potential find, increment
                                        framePos++;
                                    }
                                    else
                                    {
                                        // Not found, reset
                                        framePos = 0;
                                    }

                                    if (framePos == frameStart.Length)
                                    {
                                        if (adapter.SocketTraceSwitch.TraceVerbose)
                                            TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketFrameStartFound, ConfigurationHelper.GetHexString(frameStart));

                                        // Found
                                        framePos = 0;
                                        currentStatus = FrameStatus.MessageStartFoundCreateMessage;
                                        break;
                                    }
                                }

                                break;

                            case FrameStatus.MessageStartFoundCreateMessage:

                                if (adapter.SocketTraceSwitch.TraceVerbose)
                                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveStateStartFoundCreateMessage);

                                // Create a new memory stream and add to array
                                stream = new MemoryStream();
                                streams.Add(stream as Stream);

                                // Set next state
                                currentStatus = FrameStatus.MessageStartFound;
                                break;

                            case FrameStatus.MessageStartFound:

                                if (adapter.SocketTraceSwitch.TraceVerbose)
                                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveStateStartFound);

                                // Search for end delimiter
                                while (pos < bytesRead)
                                {
                                    if (buffer[pos++] == frameEnd[framePos])
                                    {
                                        // Potential find, increment
                                        framePos++;
                                    }
                                    else
                                    {
                                        // Did we not find the end frame?
                                        if (framePos > 0)
                                        {
                                            // Not found, write bytes we've been holding back to stream (don't forget, they may span
                                            // across more than one buffer)
                                            backFillPos = pos - (framePos + 1);

                                            if (backFillPos < 0)
                                            {
                                                // Bytes spill into previous buffer
                                                stream.Write(previousBuffer, previousBuffer.Length - (~backFillPos) - 1, (~backFillPos) + 1);
                                                stream.Write(buffer, 0, framePos - (~backFillPos) - 1);
                                            }
                                            else
                                            {
                                                // Write bytes from main buffer
                                                stream.Write(buffer, backFillPos, framePos);
                                            }

                                            // Reset
                                            framePos = 0;
                                        }
                                    }

                                    // Did we find the frame?
                                    if (framePos == frameEnd.Length)
                                    {
                                        if (adapter.SocketTraceSwitch.TraceVerbose)
                                            TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketFrameEndFound, ConfigurationHelper.GetHexString(frameEnd));

                                        // Found
                                        framePos = 0;
                                        currentStatus = FrameStatus.MessageEndFound;

                                        // If we are only accepting a single message, set flag so that we can exit loop
                                        if (!acceptMultipleMessages)
                                        {
                                            messageFound = true;
                                        }

                                        // Break out of while loop as we've found the end frame
                                        break;
                                    }
                                    else if (framePos == 0) // No potential end frame, just write the byte
                                    {
                                        // Write byte to stream
                                        stream.WriteByte(buffer[pos - 1]);
                                    }
                                }

                                break;

                            case FrameStatus.MessageEndFound:

                                if (adapter.SocketTraceSwitch.TraceVerbose)
                                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveStateEndFound, stream.Length);

                                // Set status to start looking again
                                currentStatus = FrameStatus.MessageStartNotFound;
                                break;
                        }

                        // Exit loop early
                        if (messageFound)
                        {
                            break;
                        }

                        // Did we process all of the bytes?
                        if (pos >= bytesRead)
                        {
                            if (adapter.SocketTraceSwitch.TraceVerbose)
                                TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveBufferProcessed);

                            // Copy buffer to previous buffer
                            buffer.CopyTo(previousBuffer, 0);
                            processedAllBytes = true;
                        }
                    }

                    // Did we find a message and therefore must exit read() loop (only if accepting single messages not multiple)?
                    // This avoids reading again and waiting for timeout in case there are multiple messages (we wouldn't know how many
                    // therefore must wait).
                    if (messageFound)
                    {
                        if (adapter.SocketTraceSwitch.TraceVerbose)
                            TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveExitLoopEarly);

                        break;
                    }
                }

                if (adapter.SocketTraceSwitch.TraceVerbose)
                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketNoMoreData);
            }
            catch (SocketException e)
            {
                if (e.SocketErrorCode == SocketError.TimedOut || e.SocketErrorCode == SocketError.WouldBlock)
                {
                    // Receive timed out because no more data was sent - any partial messages received will be returned
                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveTimedOut, e.Message);
                }
                else
                {
                    // Something else unexpected occurred - throw the error
                    TraceHelper.WriteEntry(TraceLevel.Error, ResourceConstants.SocketFailedReceivingData, e.Message);
                    throw (e);
                }
            }
            finally
            {
                if (streams.Count > 0)
                {
                    // Convert the data encoding into UTF-8 (unless it is already ASCII 7-bit or UTF-8)
                    for (int i = 0; i < streams.Count; i++)
                    {
                        // Reset stream
                        streams[i].Position = 0;

                        if (!(dataEncoding is ASCIIEncoding || dataEncoding is UTF8Encoding))
                        {
                            if (adapter.SocketTraceSwitch.TraceVerbose)
                                TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketEncodingConversion, dataEncoding, Encoding.UTF8);

                            // Get the bytes associated with the stream
                            byte[] bytes = new byte[streams[i].Length];
                            streams[i].Read(bytes, 0, (int)streams[i].Length);

                            // Clear original stream
                            streams[i].Close();
                            streams[i].Dispose();

                            // Write converted bytes back into stream
                            streams[i] = new MemoryStream(Encoding.Convert(dataEncoding, Encoding.UTF8, bytes));
                            streams[i].Position = 0;
                        }
                    }

                    // Set flag indicating whether the last stream wasn't complete (didn't find end frame)
                    lastStreamNotComplete = (currentStatus == FrameStatus.MessageStartFound ? true : false);
                }
            }

            // Return array
            return (streams.ToArray());
        }

        /// <summary>
        ///     This method sends data on the socket.  If a timeout occurs and it was in the middle of sending a message, it throws
        ///     a SocketException.
        /// </summary>
        /// <remarks>
        ///     Codeplex 7313: This related to the fact the stream was non seekable. Used the VirtualStream class
        ///</remarks>
        public static long SendData(IAdapter adapter, Socket socket, Stream[] streams, byte[] frameStart, byte[] frameEnd, Encoding dataEncoding, int dataBufferSize)
        {
            if (adapter.SocketTraceSwitch.TraceVerbose)
                TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketAboutToSend, ConfigurationHelper.GetHexString(frameStart), ConfigurationHelper.GetHexString(frameEnd), dataEncoding, dataBufferSize, streams.Length);

            // Buffer
            byte[] buffer = new byte[dataBufferSize];
            int bytesToSend;
            long totalBytesSent = 0;
            SeekableReadOnlyStream seekableStream;

            try
            {
                // Write each stream as a separate message
                for (int i = 0; i < streams.Length; i++)
                {
                    seekableStream = new SeekableReadOnlyStream(streams[i]);
                    if (adapter.SocketTraceSwitch.TraceVerbose)
                    {
                        // TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSendingMessage, (i + 1), streams[i].Length);
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSendingMessage, (i + 1), seekableStream.Length);
                    }
                    // Reposition stream
                    seekableStream.Position = 0;

                    // Convert stream from UTF-8 to the data encoding
                    byte[] bytes = new byte[seekableStream.Length];
                    seekableStream.Read(bytes, 0, (int)seekableStream.Length);

                    // Clear stream
                    seekableStream.Close();
                    seekableStream.Dispose();

                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketEncodingConversion, Encoding.UTF8, dataEncoding);

                    // Create a new stream with converted bytes
                    streams[i] = new MemoryStream(Encoding.Convert(Encoding.UTF8, dataEncoding, bytes));
                    streams[i].Position = 0;

                    // Write start frame
                    socket.Send(frameStart);
                    totalBytesSent += frameStart.Length;

                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSentBytes, frameStart.Length);

                    // Run through stream
                    while ((bytesToSend = streams[i].Read(buffer, 0, dataBufferSize)) > 0)
                    {
                        // Send data to socket
                        socket.Send(buffer, bytesToSend, SocketFlags.None);
                        totalBytesSent += bytesToSend;

                        if (adapter.SocketTraceSwitch.TraceVerbose)
                            TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSentBytes, bytesToSend);
                    }

                    // Write end frame
                    socket.Send(frameEnd);
                    totalBytesSent += frameEnd.Length;

                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSentBytes, frameEnd.Length);

                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSentMessage, (i + 1));
                }
            }
            catch (SocketException e)
            {
                TraceHelper.WriteEntry(TraceLevel.Error, ResourceConstants.SocketFailedSendingData, e.Message);
                throw (e);
            }

            return (totalBytesSent);
        }

        /// <summary>
        ///     This method receives data from one socket and sends the same data to another.
        /// </summary>
        public static long ReceiveSendData(IAdapter adapter, Socket receiveSocket, Socket sendSocket, int dataBufferSize)
        {
            if (adapter.SocketTraceSwitch.TraceVerbose)
                TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketAboutToReceiveSend, dataBufferSize);

            // Buffer
            byte[] buffer = new byte[dataBufferSize];
            int bytesRead = 0;
            long totalBytesSent = 0;

            try
            {
                // Read from the socket
                while ((bytesRead = receiveSocket.Receive(buffer)) > 0)
                {
                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveBytes, bytesRead);

                    // Send data to socket
                    sendSocket.Send(buffer, 0, bytesRead, SocketFlags.None);
                    totalBytesSent += bytesRead;

                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketSentBytes, bytesRead);
                }

                if (adapter.SocketTraceSwitch.TraceVerbose)
                    TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketNoMoreData);
            }
            catch (SocketException e)
            {
                if (e.SocketErrorCode == SocketError.TimedOut)
                {
                    // Receive timed out because no more data was sent
                    if (adapter.SocketTraceSwitch.TraceVerbose)
                        TraceHelper.WriteEntry(TraceLevel.Verbose, ResourceConstants.SocketReceiveTimedOut, e.Message);
                }
                else
                {
                    // Something else unexpected occurred - throw the error
                    TraceHelper.WriteEntry(TraceLevel.Error, ResourceConstants.SocketFailedReceivingData, e.Message);
                    throw (e);
                }
            }

            return (totalBytesSent);
        }

        #endregion
    }
}
